CONSOLE-5296: Add non-scalable image warning when scaling workloads#16436
CONSOLE-5296: Add non-scalable image warning when scaling workloads#16436swshende-cmd wants to merge 6 commits into
Conversation
|
@swshende-cmd: This pull request references RFE-3935 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the feature request to target the "5.0.0" version, but no target version was set. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
|
Hi @swshende-cmd. Thanks for your PR. I'm waiting for a openshift member to verify that this patch is reasonable to test. If it is, they should reply with Regular contributors should join the org to skip this step. Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
📝 WalkthroughWalkthroughAdds a hook to detect ImageStreamTags labeled ChangesNon-scalable image detection and UI warnings
sequenceDiagram
participant UI as UI (PodRing / Modal)
participant Hook as useNonScalableImageCheck
participant API as kube API / k8sGet
participant IST as ImageStreamTag
UI->>Hook: provide resource
Hook->>Hook: parse triggers/annotation -> IST ref?
alt IST ref found
Hook->>API: k8sGet(ImageStreamTag)
API->>IST: fetch IST
IST-->>API: IST data (includes dockerImageMetadata.Config.Labels)
API-->>Hook: IST payload
Hook->>Hook: inspect label io.openshift.non-scalable (true/'true')
Hook-->>UI: {isNonScalable: true, loading:false}
UI->>UI: show tooltip/alert when user increases replicas >1
else no IST ref or error
Hook-->>UI: {isNonScalable:false, loading:false}
end
Estimated code review effort 🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 12✅ Passed checks (12 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/packages/console-shared/src/hooks/useNonScalableImageCheck.ts`:
- Line 71: The effect is unstable because it depends on the istRef object
reference (const istRef = useMemo(() => getISTReference(resource), [resource]))
which changes when resource refreshes even if name/namespace are identical;
update the effect dependency to use stable primitives instead (e.g. istRef.name
and istRef.namespace or a derived string key) so the k8sGet call only runs when
the IST identity actually changes; locate getISTReference/istRef and the effect
that calls k8sGet and replace the istRef object dependency with those stable
properties or a memoized string.
- Around line 20-21: The current logic in useNonScalableImageCheck.ts only
examines the first annotation trigger and accepts any DeploymentConfig
ImageChange trigger without verifying its kind, which can mask a valid
ImageStreamTag (IST) trigger elsewhere; update the code that finds
imageChangeTrigger (the triggers.find call) to instead iterate all triggers and
select one whose type === 'ImageChange' and whose imageChangeParams.from.kind
=== 'ImageStreamTag' (or validate kind before using .name), and likewise update
the annotation-trigger handling to scan all annotation entries (not just the
first) to detect any IST annotations before resolving the IST; ensure you
reference and modify the imageChangeTrigger selection and the annotation parsing
logic so only true IST triggers are treated as valid.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Enterprise
Run ID: 606ab21e-dc84-48b0-8c21-1d33ecbb5d2b
📒 Files selected for processing (7)
frontend/packages/console-shared/locales/en/console-shared.jsonfrontend/packages/console-shared/src/components/pod/PodRing.tsxfrontend/packages/console-shared/src/hooks/__tests__/useNonScalableImageCheck.spec.tsfrontend/packages/console-shared/src/hooks/useNonScalableImageCheck.tsfrontend/public/components/modals/__tests__/configure-count-modal.spec.tsxfrontend/public/components/modals/configure-count-modal.tsxfrontend/public/locales/en/public.json
📜 Review details
🔇 Additional comments (6)
frontend/public/locales/en/public.json (1)
1756-1757: LGTM!frontend/packages/console-shared/locales/en/console-shared.json (1)
234-234: LGTM!frontend/public/components/modals/configure-count-modal.tsx (1)
46-47: LGTM!Also applies to: 101-112
frontend/packages/console-shared/src/components/pod/PodRing.tsx (1)
3-3: LGTM!Also applies to: 10-10, 86-86, 131-157
frontend/public/components/modals/__tests__/configure-count-modal.spec.tsx (1)
1-107: LGTM!frontend/packages/console-shared/src/hooks/__tests__/useNonScalableImageCheck.spec.ts (1)
1-147: LGTM!
|
@swshende-cmd: Jira verification commands are restricted to collaborators for this repo. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
|
/verified by @swshende-cmd |
|
@dpateriya: This PR has been marked as verified by DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
|
/ok-to-test |
|
/retest |
|
@swshende-cmd: This pull request references RFE-3935 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the feature request to target the "5.0.0" version, but no target version was set. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
|
/retest |
|
/verified by @swshende-cmd |
|
@dpateriya: This PR has been marked as verified by DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
jhadvig
left a comment
There was a problem hiding this comment.
Thanks for the contribution! The feature design is sound — the IST resolution logic for both DeploymentConfig triggers and Deployment annotation triggers is well done, and the error handling is sensible. A few structural issues to address before this can merge, mostly around aligning with project conventions.
| import * as _ from 'lodash'; | ||
| import { ImageStreamTagModel } from '@console/internal/models'; | ||
| import type { K8sResourceKind } from '@console/internal/module/k8s'; | ||
| import { k8sGet } from '@console/internal/module/k8s'; |
There was a problem hiding this comment.
Use useK8sWatchResource instead of imperative k8sGet
Every other data-fetching hook in console-shared/src/hooks/ uses the declarative useK8sWatchResource — including useRelatedHPA, the nearest peer hook also consumed by PodRing. Using k8sGet inside a useEffect with manual cancellation means:
- No live updates if the IST label changes while the page is open
- No shared cache — each component instance fires its own fetch
- Manual
cancelledflag boilerplate that the framework handles automatically
Suggested approach:
const [ist, loaded, error] = useK8sWatchResource<K8sResourceKind>(
istName && istNamespace
? { kind: ImageStreamTagModel.kind, name: istName, namespace: istNamespace, isList: false }
: null,
);
const isNonScalable = loaded && !error
? ist?.image?.dockerImageMetadata?.Config?.Labels?.['io.openshift.non-scalable'] === 'true'
: false;
return { isNonScalable, loading: !loaded };This eliminates the useState, useEffect, and cancellation logic entirely.
| const istName = istRef?.name; | ||
| const istNamespace = istRef?.namespace; | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
useMemo depends on unstable resource object reference
resource is a K8s watch object whose reference changes on every watch cycle. This useMemo recomputes on every render, making it effectively a no-op. The downstream istName/istNamespace primitives save the useEffect from re-firing, but getISTReference() (which parses triggers and JSON-parses annotations) runs redundantly on every render.
If you switch to useK8sWatchResource (comment above), this becomes moot. If keeping the current approach, depend on stable primitives:
const triggerAnnotation = resource?.metadata?.annotations?.[IMAGE_TRIGGER_ANNOTATION];
const resourceKind = resource?.kind;
const resourceNamespace = resource?.metadata?.namespace;
const istRef = useMemo(
() => getISTReference(resource),
[resourceKind, resourceNamespace, triggerAnnotation],
);| onClick={() => handleClick(1)} | ||
| isBlock | ||
| /> | ||
| {isNonScalable && clickCount >= 1 ? ( |
There was a problem hiding this comment.
Deduplicate the Button JSX
The entire <Button> (8 identical prop lines) is duplicated — once with a <Tooltip> wrapper, once without. Extract the button and conditionally wrap:
const scaleUpButton = (
<Button
icon={<AngleUpIcon style={{ fontSize: '20' }} />}
type="button"
variant="plain"
aria-label={t('console-shared~Increase the Pod count')}
title={t('console-shared~Increase the Pod count')}
onClick={() => handleClick(1)}
isBlock
/>
);
// Then in the JSX:
{isNonScalable && clickCount >= 1 ? (
<Tooltip content={t('console-shared~This image is not intended to run with more than one replica. Scaling up may cause unexpected behavior.')}>
{scaleUpButton}
</Tooltip>
) : scaleUpButton}| } = obj; | ||
| const [hpa] = useRelatedHPA(apiVersion, kind, name, namespace); | ||
| const hpaControlledScaling = !!hpa; | ||
| const { isNonScalable } = useNonScalableImageCheck(obj); |
There was a problem hiding this comment.
loading state is ignored — user can scale before the check completes
The hook returns { isNonScalable, loading } but both consumers (PodRing and ConfigureCountModal) destructure only isNonScalable. During the initial fetch, isNonScalable defaults to false, so the warning won't show until the IST fetch resolves. If the check matters enough to exist, consider disabling the scale-up button or showing a spinner while loading is true, to avoid a brief window where the user can scale without seeing the warning.
| mockUseNonScalableImageCheck.mockReturnValue({ isNonScalable: false, loading: false }); | ||
| }); | ||
|
|
||
| it('should show warning when image is non-scalable and value > 1', () => { |
There was a problem hiding this comment.
Missing test for the primary user flow: spinning from 1 to 2
All four tests check initial render states, but the most important scenario is untested: a user opens the modal at 1 replica and uses the spinner to increase to 2. That's when the warning should dynamically appear. Consider adding a test using userEvent to click the increment button and assert the warning becomes visible.
| const { t } = useTranslation(); | ||
| const [handlePromise, inProgress, errorMessage] = usePromiseHandler(); | ||
| const isReplicaPath = path === '/spec/replicas'; | ||
| const { isNonScalable } = useNonScalableImageCheck(resource); |
There was a problem hiding this comment.
Hook runs unconditionally even for non-replica paths (Jobs, etc.)
useNonScalableImageCheck will resolve IST triggers and fetch the ImageStreamTag even when path is /spec/parallelism (Jobs) where the warning is never shown. Consider gating the hook to avoid unnecessary API calls:
const { isNonScalable } = useNonScalableImageCheck(isReplicaPath ? resource : null);The hook already handles null/missing triggers gracefully, so passing null when the check isn't needed would skip the fetch entirely. (Same applies to PodRing — the hook runs for every workload on the topology view, including bare Deployments without IST triggers.)
| @@ -0,0 +1,114 @@ | |||
| import { useState, useEffect, useMemo } from 'react'; | |||
| import * as _ from 'lodash'; | |||
There was a problem hiding this comment.
Nit: Full lodash import for a single _.get call. Only _.get(ist, 'image.dockerImageMetadata.Config.Labels', {}) is used. Consider optional chaining instead, which eliminates the lodash dependency entirely:
const labels = ist?.image?.dockerImageMetadata?.Config?.Labels ?? {};(If you adopt useK8sWatchResource per the other comment, this becomes even simpler since there's no .then() callback at all.)
| const mockUseNonScalableImageCheck = useNonScalableImageCheck as jest.Mock; | ||
|
|
||
| const baseProps = { | ||
| defaultValue: 0, |
There was a problem hiding this comment.
Minor: defaultValue: 0 is misleading here. The first test overrides replicas: 2, so value initializes to 2 (from _.get(resource, getPath)). The defaultValue: 0 is never reached. Meanwhile baseProps has replicas: 1, making defaultValue: 0 dead code there too. Setting defaultValue: 1 would better reflect the actual test scenarios and avoid confusion for future readers.
| onClick={() => handleClick(1)} | ||
| isBlock | ||
| /> | ||
| {isNonScalable && clickCount >= 1 ? ( |
There was a problem hiding this comment.
Minor: Tooltip shows before the user has done anything. clickCount is initialized to obj.spec.replicas. When a workload is already at 1 replica, the tooltip appears on the up-arrow immediately — before the user has interacted. This might be intentional (preemptive warning), but worth confirming. If the intent is to warn only after attempting to scale up, the condition should be clickCount > 1 instead.
|
Also worth to mention that the correct workflow is to create CONSOLE project story from the RFE and reference that story. |
Warn users when scaling a workload beyond one replica if its container image has the io.openshift.non-scalable=true label. The warning appears as an inline alert in the Edit Pod count modal and as a tooltip on the PodRing scale-up button. Adds a useNonScalableImageCheck hook that resolves ImageStreamTag references from Deployment trigger annotations or DeploymentConfig ImageChange triggers, fetches the IST, and inspects image.dockerImageMetadata.Config.Labels for the non-scalable label. Co-authored-by: Cursor <cursoragent@cursor.com>
- Validate ImageStreamTag kind in DeploymentConfig triggers before resolving - Scan all annotation triggers instead of only the first one - Use stable primitives (name, namespace) as effect dependencies Co-authored-by: Cursor <cursoragent@cursor.com>
Moves new i18n keys to the parser-determined position and removes orphaned keys no longer referenced after the modal rewrite on main. Co-authored-by: Cursor <cursoragent@cursor.com>
Move Non-scalable image keys to parser-determined position (after "Please enter a URL." at line 874) and restore all existing keys. Remove trailing newline from console-shared.json to match parser output. Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace imperative k8sGet with declarative useK8sWatchResource in useNonScalableImageCheck hook for live updates and shared cache - Stabilize useMemo dependencies using primitive values instead of object references to avoid recomputing on every watch cycle - Remove lodash dependency in favor of optional chaining - Accept null resource param to skip the watch for non-replica paths - Gate hook call with isReplicaPath in configure-count-modal - Deduplicate scale-up Button JSX in PodRing with shared variable - Add comment explaining preemptive tooltip at clickCount >= 1 - Rewrite hook tests to mock useK8sWatchResource instead of k8sGet - Add userEvent increment test and fix defaultValue to 1 in modal tests Co-authored-by: Cursor <cursoragent@cursor.com>
|
@swshende-cmd: This pull request references CONSOLE-5296 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.0.0" version, but no target version was set. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
|
@jhadvig All the changes suggested are done and also verified / tested by me as per the screen prints attached . Please help with your valuable review to further proceed. Also the console story is : https://redhat.atlassian.net/browse/CONSOLE-5296 assigned to me.
Awaiting your kind response. |
|
/retest e2e-gcp-console |
|
/test e2e-gcp-console |
QA Verification Evidence
Verification Steps
Warning This verification was performed by an AI agent. Results may contain false positives or miss Automated QA verification by Claude Code |
jhadvig
left a comment
There was a problem hiding this comment.
Thanks @swshende-cmd adding comments for two minor issues. otherwise looks good to go 👍
|
|
||
| const isNonScalable = | ||
| loaded && !error | ||
| ? (ist as any)?.image?.dockerImageMetadata?.Config?.Labels?.[NON_SCALABLE_LABEL] === 'true' |
| () => (resource ? getISTReference(resource) : null), | ||
| // Depend on stable primitives to avoid recomputing on every watch cycle | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [resourceKind, resourceNamespace, triggerAnnotation, dcTriggers], |
There was a problem hiding this comment.
The problem is that dcTriggers is resource?.spec?.triggers an array reference that changes on every K8s watch cycle, even if the content is identical. The other three deps (resourceKind, resourceNamespace, triggerAnnotation) are all strings, so they're stable...
The fix: extract the one piece of data you actually care about from the triggers array — the IST name — as a string:
const dcISTName = resource?.spec?.triggers?.find(
(t) =>
t?.type === 'ImageChange' &&
t?.imageChangeParams?.from?.kind === 'ImageStreamTag',
)?.imageChangeParams?.from?.name;
const istRef = useMemo(
() => (resource ? getISTReference(resource) : null),
// eslint-disable-next-line react-hooks/exhaustive-deps
[resourceKind, resourceNamespace, triggerAnnotation, dcISTName],
);- Replace `(ist as any)` with a proper `ImageStreamTagResource` type that extends K8sResourceKind with the IST-specific `image` property - Replace `dcTriggers` array dep (unstable reference) with `dcISTName` string primitive extracted from the triggers array Co-authored-by: Cursor <cursoragent@cursor.com>
|
@jhadvig I have made necessary changes, hope we are good to go ahead now |
|
/lgtm |
|
@jhadvig: This PR has been marked as verified by DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: jhadvig, swshende-cmd The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
@swshende-cmd: all tests passed! Full PR test history. Your PR dashboard. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here. |



















Summary
io.openshift.non-scalable=truelabel in its metadatauseNonScalableImageCheckhook that resolves ImageStreamTag references from Deployment trigger annotations or DeploymentConfig ImageChange triggers, watches the IST viauseK8sWatchResource, and checksimage.dockerImageMetadata.Config.Labelsfor the non-scalable labelJIRA
CONSOLE-5296 (created from RFE-3935)
Changes
frontend/packages/console-shared/src/hooks/useNonScalableImageCheck.tsuseK8sWatchResourceto check forio.openshift.non-scalablelabel on an ImageStreamTagfrontend/public/components/modals/configure-count-modal.tsxisReplicaPathfrontend/packages/console-shared/src/components/pod/PodRing.tsxfrontend/public/locales/en/public.jsonfrontend/packages/console-shared/locales/en/console-shared.jsonfrontend/packages/console-shared/src/hooks/__tests__/useNonScalableImageCheck.spec.tsfrontend/public/components/modals/__tests__/configure-count-modal.spec.tsxScreenshots / Recordings
To be added by author
Test plan
yarn test packages/console-shared/src/hooks/__tests__/useNonScalableImageCheck.spec.ts(9 tests)yarn test public/components/modals/__tests__/configure-count-modal.spec.tsx(6 tests)Made with Cursor
Summary by CodeRabbit
New Features
Localization
Tests